<?php
require 'HTTPSocket.php';

class XMLHttpRequest{
	public function __set($property, $value){
		$property = strtolower($property);
		switch ($property)
		{
			case "port":
			case "socket":
			case "error":
			case "readystate":
			case "responsetext":
			case "responsexml":
			case "status":
			case "statustext":
				throw new Exception("property \"$property\" cannot be assigned to -- it is read only");
				break;
			default:
				throw new Exception("class \"".__CLASS__."\" does not contain a definition for \"$property\"");
		}
	}
	
	public function __get($property){
		$property = strtolower($property);

		switch ($property)
		{
			case "port":
			case "socket":
				return $this->socket;
			case "error":
				return "ERROR";
			case "maxredirects":
			case "readystate":
			case "responsetext":
				if($this->status==OPENED || $this->status==UNSENT){
					return 0;
				}

				if($this->error_flag==true){
					return "";
				}

				return $this->responseText;
			case "status":
				if($this->status==OPENED || $this->status==UNSENT){
					return 0;
				}

				if($this->error_flag==true){
					return "";
				}

				return $this->status_code;
			case "statustext":
				if($this->status==OPENED || $this->status==UNSENT || $this->error_flag==true){
					return "";
				}

				return $this->statusText;
			case "responsexml":
				throw new Exception("ResponseXML is currently not supported");
				return NULL;
			default:
				throw new Exception("class \"".__CLASS__."\" does not contain a definition for \"$property\"");
		}
	}
	
	public function __construct(){
		
	}
	
	public function __destruct(){
		$this->socket=NULL;
	}
	
	public function __toString(){
		return __CLASS__;
	}

	public function setRequestHeader($label, $value){
		if($this->status!=OPENED || $this->send_flag==true){
			throw new Exception("INVALID_STATE_ERR");
		}
		//	If header does not match the field-name production raise a SYNTAX_ERR exception and terminate these steps.
		//	If the value argument does not match the field-value production raise a SYNTAX_ERR and terminate these steps.
		
		//	For security reasons, these steps should be terminated if header is an ASCII case-insensitive match for one of the following headers:
		//	SHOULD BE CONTROLLED BY USER AGENT
		/*
		Accept-Charset
		Accept-Encoding
		Connection
		Content-Length
		Cookie
		Cookie2
		Content-Transfer-Encoding
		Date
		Expect
		Host
		Keep-Alive
		Referer
		TE
		Trailer
		Transfer-Encoding
		Upgrade
		User-Agent
		Via
		
		or if the start of header is an ASCII case-insensitive match for Proxy- or Sec- (including when header is just Proxy- or Sec-).
		*/
		
		//	Subsiquent values should be appended to the header!
		$this->headers[$label]=$value;
	}
	
	public function getAllResponseHeaders(){
		if($this->status==OPENED || $this->status==UNSENT || $this->error_flag==true){
			return "";
		}
		return $this->responseHeaders;
	}
	
	public function getResponseHeader($label){
		if($this->status==OPENED || $this->status==UNSENT || $this->error_flag==true){
			return null;
		}
		
		$value = array();
		preg_match_all('/^(?s)'.$label.': (.*?)\r\n/im', $this->responseHeaders, $value);
		
		if(count($value ) > 0)
		{
			return implode(', ', $value[1]);
		}
		return NULL;
	}
	
	//	XMLTTHP Spec Properties
	//	TODO - hanlder for onreadystatechange
	const UNSENT = 0;
	const OPENED = 1;
	const HEADERS_RECEIVED = 2;
	const LOADING = 3;
	const DONE = 4;
	
	const SECURITY_ERR = 18;
	const NETWORK_ERR = 19;
	const ABORT_ERR = 20;
	
	protected $readyState;
	protected $status=UNSENT;
	protected $statusText;
	protected $responseText;
	protected $responseXML;
	
	//	Flags
	private $send_flag=false;
	private $error_flag=false;
	
	//	Internal Properties
	private $responseHeaders;
	private $headers=array();
	private $is_safe_mode = true;
	private $properties = array();
	
	private $method;
	private $protocol;
	private $port;
	private $host;
	private $url;
	private $async;
	private $user;
	private $password;
	
	private $socket;
	private $connected = 0;
	private $status_code;
	
	public function open($method, $url, $async = false, $user = "", $password = ""){
		$valid_url=preg_match("/^(http[s]?)[\:]\/\/([a-z0-9\.\-\_\d]{1,})([\:]([0-9]{1,}))?(\/[\/\.\S\d]{1,})?(\?)?/", $url,$matches);

		$this->url="/";

		if($valid_url){
			$this->protocol=$matches[1];
			$this->host=$matches[2];
			if(empty($matches[3])){$this->port="80";}else{$this->port=$matches[4];}
			$this->url=$matches[5];
		}else{
			throw new Exception("Invalid URL.");
		}
		
		$this->properties = array("readystate" => 0);
		
		//	If method is an ASCII case-insensitive match for CONNECT, DELETE, GET, HEAD, OPTIONS, POST, PUT, TRACE, or TRACK convert method to ASCII uppercase.
		// && !empty($url)
		
		if(!empty($method))
		{
			$method = strtoupper(trim($method));
			if(!preg_match("/^(CONNECT|DELETE|GET|HEAD|OPTIONS|POST|PUT|TRACE|TRACK)$/", $method))
			{
				throw new Exception("Unknown HTTP method \"$method\"");
			}
		}else{
			throw new Exception("A valid HTTP method must be provided");
		}
		
		if(empty($url)){
			throw new Exception("The URL for this request can not be empty");
		}
		
		// if(!empty($user) && empty($password))
		// 	{
		// 		throw new Exception("Basic Authentication over HTTP requires both a username and a password");
		// 	}
		
		//	Abort any current sends!
		$this->send_flag=false;
		
		$this->headers = array();
		$this->method = $method;
		
		$this->user=$user;
		$this->password=$password;
		$this->responseHeaders;	
		$this->async=$async;

		$this->status=OPENED;
	}
	
	public function send($data = NULL){
		//	Test the connection
		if ( $this->socket = new HTTPSocket($this->host,$this->port) ) {
					
		}else {
			 // NETWORK_ERR
			$this->responseText=NULL;
			$this->error_flag=true;
			$this->headers = array();
			$this->status=DONE;
			$this->socket->close();
			$this->socket=NULL;
			
			throw new Exception("NETWORK_ERR");
		}
		
		if($this->socket!=NULL){
			// //	If the state is not opened run a INVALID_STATE_ERR		
			// 	//	If the state_flag==true run a  INVALID_STATE_ERR 
			if($this->status!=OPENED || $this->send_flag==true){
				throw new Exception("INVALID_STATE_ERR");
			}
			// 
			// 	//	If the request method is GET or HEAD act as if data is null.
			// 
			// 	//	If the asynchronous flag is false release the storage mutex.
			// 
			// 
			// 	//	Set the error flag to false.
			$this->error_flag=false;		
			
			// 
			$url = str_replace(' ', '%20', $url);
			
			$out = $this->method." ".$this->url." HTTP/1.0\r\n";
			$out .= "Host: ".$this->host.":".$this->port."\r\n";
		
			//	Add basic auth headers
			if(!empty($this->user) && !empty($this->password))
			{
				$auth= "Basic ".base64_encode($this->user.':'.$this->password);
				$this->setRequestHeader('Authorization',$auth);
			}

			if($data != null){
				$this->setRequestHeader('Content-Length',strlen($data));
			}else{
				$data="";
			}
				
			foreach ( $this->headers as $name => $value ) {
				$out .= $name.': '.$value."\r\n";
			}

			$out .= "\r\n";
			$out .= $data;
			$out .= "\r\n";
		
			$response = $this->socket->send($out, 'string');
			
			//	$this->status=HEADERS_RECEIVED;
			//	$this->status=LOADING;
			
			$this->status=DONE;
			
			$this->build_response($response);
		}
	}

	public function abort(){
		
	}
	
	private function build_response($response){
		$lines=preg_split("/\r\n/", $response);
		$this->responseHeaders=array();	
		$first=true;
		$start_body=false;
		$body="";
		
		foreach ( $lines as $line) {
			if($first==true){
				$this->statusText=$line;
				
				$tokens = explode(" ",$line);
				$this->status_code=intval($tokens[1]);
				$first=false;
			}else{
				if($start_body==true){
					$body.=$line;
				}else{
					$line=str_replace("\n","",$line);
					$line=str_replace("\r","",$line);
					
					if(strlen($line)!=0){
						$pattern = '/^([a-z0-9A-Z\-]{1,}):/';
						preg_match($pattern, $line, $matches);
						$value=str_replace($matches[0],"",$line);
						$key=substr($matches[0], 0, -1);
						$this->responseHeaders[$key]=$value;
					}else{
						$start_body=true;
					}
				}
			}
			$this->responseText=$body;
		}
	}
}
?>